// ==UserScript==
// @name         Geocaching: Reveal Hidden Text
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Reveals hidden / invisible clue text inline only inside UserSuppliedContent, including matching text/background colors
// @copyright    2026
// @author       OUT14ND3R
// @match        https://www.geocaching.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let revealed = false;
    const originalStyles = new WeakMap();

    function getUSCRoots() {
        return Array.from(document.querySelectorAll("div.UserSuppliedContent"));
    }

    function textOf(el) {
        return (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim();
    }

    function parseRGB(color) {
        const m = color && color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
        return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
    }

    function colorDistance(a, b) {
        if (!a || !b) return 999;
        return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
    }

    function effectiveBackground(el) {
        let cur = el;

        while (cur && cur !== document.documentElement) {
            const bg = getComputedStyle(cur).backgroundColor;
            if (bg && !/rgba?\(0,\s*0,\s*0,\s*0\)|transparent/i.test(bg)) {
                return bg;
            }
            cur = cur.parentElement;
        }

        return getComputedStyle(document.body).backgroundColor || "rgb(255,255,255)";
    }

    function textColorMatchesBackground(el) {
        const cs = getComputedStyle(el);
        const fg = parseRGB(cs.color);
        const bg = parseRGB(effectiveBackground(el));

        if (!fg || !bg) return false;

        return colorDistance(fg, bg) <= 15;
    }

    function isScriptBadge(el) {
        return (
            el.classList &&
            (
                el.classList.contains("gch-inline-hidden-badge") ||
                el.classList.contains("gch-inline-comment-badge")
            )
        );
    }

    function isIgnored(el) {
        if (isScriptBadge(el)) return true;

        return [
            "SCRIPT", "STYLE", "META", "LINK", "HEAD", "TITLE", "NOSCRIPT",
            "SVG", "PATH"
        ].includes(el.tagName);
    }

    function isIgnoredAttribute(name, value) {
        if (!value) return true;

        const clean = value.replace(/\s+/g, " ").trim();

        // Ignore Geocaching solver-list tooltip.
        // This prevents showing: TITLE: Click to see the list of solvers
        if (name === "title" && /^Click to see the list of solvers$/i.test(clean)) {
            return true;
        }

        return false;
    }

    function isLikelyHidden(el) {
        if (isIgnored(el)) return false;

        const txt = textOf(el);
        if (!txt) return false;

        const cs = getComputedStyle(el);
        const style = (el.getAttribute("style") || "").toLowerCase();
        const classId = ((el.className || "") + " " + (el.id || "")).toString().toLowerCase();

        if (el.hidden) return true;
        if (el.getAttribute("aria-hidden") === "true") return true;

        if (cs.display === "none") return true;
        if (cs.visibility === "hidden" || cs.visibility === "collapse") return true;
        if (Number(cs.opacity) === 0) return true;

        if (parseFloat(cs.fontSize) <= 1) return true;
        if (parseFloat(cs.lineHeight) === 0) return true;

        if (/display\s*:\s*none/.test(style)) return true;
        if (/visibility\s*:\s*hidden/.test(style)) return true;
        if (/opacity\s*:\s*0/.test(style)) return true;
        if (/font-size\s*:\s*0/.test(style)) return true;

        if (/txt_hidden|hidden|invisible|spoiler|secret|clue/.test(classId)) return true;

        if (/text-indent\s*:\s*-\d{3,}/.test(style)) return true;
        if (parseFloat(cs.textIndent) < -500) return true;

        const rect = el.getBoundingClientRect();
        if (rect.left < -500 || rect.top < -500) return true;

        if (textColorMatchesBackground(el)) return true;

        return false;
    }

    function hasUsefulAttribute(el) {
        if (isIgnored(el)) return false;

        const attrs = [
            ["alt", el.getAttribute("alt")],
            ["title", el.getAttribute("title")],
            ["aria-label", el.getAttribute("aria-label")]
        ];

        return attrs.some(([name, value]) => {
            if (isIgnoredAttribute(name, value)) return false;
            const clean = value.replace(/\s+/g, " ").trim();
            return clean.length >= 2;
        });
    }

    function hasClassAttribute(el) {
        if (isIgnored(el)) return false;

        const classValue = el.getAttribute("class");
        if (classValue === null) return false;

        if (/\bgch-inline-hidden-badge\b/i.test(classValue)) return false;
        if (/\bgch-inline-comment-badge\b/i.test(classValue)) return false;

        // Do not show CLASS: Pointer
        if (/\bPointer\b/i.test(classValue)) return false;

        return true;
    }

    function getHTMLCommentsInUSC() {
        const comments = [];

        getUSCRoots().forEach(root => {
            const walker = document.createTreeWalker(
                root,
                NodeFilter.SHOW_COMMENT,
                null
            );

            let node;
            while ((node = walker.nextNode())) {
                const txt = node.nodeValue.trim();
                if (txt) comments.push(node);
            }
        });

        return comments;
    }

    function scanHiddenInUSC() {
        const found = [];

        getUSCRoots().forEach(root => {
            root.querySelectorAll("*").forEach(el => {
                if (isLikelyHidden(el)) {
                    found.push(el);
                }
            });
        });

        return found;
    }

    function scanAttributesInUSC() {
        const found = [];

        getUSCRoots().forEach(root => {
            root.querySelectorAll("*").forEach(el => {
                if (hasUsefulAttribute(el)) {
                    found.push(el);
                }
            });
        });

        return found;
    }

    function scanClassAttributesInUSC() {
        const found = [];

        getUSCRoots().forEach(root => {
            root.querySelectorAll("*").forEach(el => {
                if (hasClassAttribute(el)) {
                    found.push(el);
                }
            });
        });

        return found;
    }

    function hiddenTextExistsInUSC() {
        return (
            scanHiddenInUSC().length > 0 ||
            getHTMLCommentsInUSC().length > 0 ||
            scanAttributesInUSC().length > 0 ||
            scanClassAttributesInUSC().length > 0
        );
    }

    function revealElement(el) {
        if (!originalStyles.has(el)) {
            originalStyles.set(el, el.getAttribute("style"));
        }

        el.removeAttribute("hidden");
        el.removeAttribute("aria-hidden");
        el.setAttribute("data-gch-revealed", "true");

        el.style.setProperty("display", "inline-block");
        el.style.setProperty("visibility", "visible");
        el.style.setProperty("opacity", "1");
        el.style.setProperty("font-size", "14px");
        el.style.setProperty("line-height", "normal");
        el.style.setProperty("text-indent", "0");
        el.style.setProperty("position", "relative");
        el.style.setProperty("color", "black");
        el.style.setProperty("background", "yellow");
        el.style.setProperty("border", "1px dashed red");
        el.style.setProperty("padding", "2px 4px");
    }

    function makeInlineBadge(label, text) {
        const badge = document.createElement("span");
        badge.className = "gch-inline-hidden-badge";
        badge.textContent = ` ${label}: ${text} `;

        Object.assign(badge.style, {
            display: "inline-block",
            background: "#ffff00",
            color: "#000",
            border: "2px dashed red",
            padding: "2px 5px",
            margin: "2px 4px",
            fontWeight: "bold",
            fontSize: "14px",
            lineHeight: "normal",
            whiteSpace: "pre-wrap"
        });

        return badge;
    }

    function revealAttributesInline(el) {
        if (el.getAttribute("data-gch-attr-revealed") === "true") return;

        const pieces = [];

        const alt = el.getAttribute("alt");
        const title = el.getAttribute("title");
        const aria = el.getAttribute("aria-label");

        if (!isIgnoredAttribute("alt", alt) && alt.trim()) pieces.push(["ALT", alt.trim()]);
        if (!isIgnoredAttribute("title", title) && title.trim()) pieces.push(["TITLE", title.trim()]);
        if (!isIgnoredAttribute("aria-label", aria) && aria.trim()) pieces.push(["ARIA", aria.trim()]);

        pieces.forEach(([label, text]) => {
            const badge = makeInlineBadge(label, text);
            el.insertAdjacentElement("afterend", badge);
        });

        el.setAttribute("data-gch-attr-revealed", "true");
    }

    function revealClassInline(el) {
        if (el.getAttribute("data-gch-class-revealed") === "true") return;
        if (isScriptBadge(el)) return;

        const classValue = el.getAttribute("class");
        if (classValue === null) return;

        if (/\bgch-inline-hidden-badge\b/i.test(classValue)) return;
        if (/\bgch-inline-comment-badge\b/i.test(classValue)) return;

        // Do not show CLASS: Pointer
        if (/\bPointer\b/i.test(classValue)) return;

        let displayText;

        if (classValue.trim() === "") {
            displayText = `class=""`;
        } else {
            displayText = classValue.trim();
        }

        const badge = makeInlineBadge("CLASS", displayText);
        el.insertAdjacentElement("afterend", badge);

        el.setAttribute("data-gch-class-revealed", "true");
    }

    function revealCommentsInline() {
        const comments = getHTMLCommentsInUSC();

        comments.forEach(comment => {
            const text = comment.nodeValue.trim();
            if (!text) return;

            const next = comment.nextSibling;
            if (
                next &&
                next.nodeType === Node.ELEMENT_NODE &&
                next.classList.contains("gch-inline-comment-badge")
            ) {
                return;
            }

            const badge = makeInlineBadge("COMMENT", text);
            badge.classList.add("gch-inline-comment-badge");

            comment.parentNode.insertBefore(badge, comment.nextSibling);
        });
    }

    function revealHiddenTextInUSC() {
        scanHiddenInUSC().forEach(el => revealElement(el));
        scanAttributesInUSC().forEach(el => revealAttributesInline(el));
        scanClassAttributesInUSC().forEach(el => revealClassInline(el));
        revealCommentsInline();

        revealed = true;
    }

    function undoReveal() {
        getUSCRoots().forEach(root => {
            root.querySelectorAll("[data-gch-revealed='true']").forEach(el => {
                const oldStyle = originalStyles.get(el);

                if (oldStyle === null || oldStyle === undefined) {
                    el.removeAttribute("style");
                } else {
                    el.setAttribute("style", oldStyle);
                }

                el.removeAttribute("data-gch-revealed");
            });

            root.querySelectorAll("[data-gch-attr-revealed='true']").forEach(el => {
                el.removeAttribute("data-gch-attr-revealed");
            });

            root.querySelectorAll("[data-gch-class-revealed='true']").forEach(el => {
                el.removeAttribute("data-gch-class-revealed");
            });

            root.querySelectorAll(".gch-inline-hidden-badge").forEach(el => {
                el.remove();
            });
        });

        revealed = false;
    }

    function removeButton() {
        const btn = document.getElementById("btnRevealHiddenText");
        if (btn) btn.remove();
    }

    function addButtonOnlyIfHiddenFound() {
        if (!getUSCRoots().length) {
            removeButton();
            return;
        }

        if (!hiddenTextExistsInUSC()) {
            removeButton();
            return;
        }

        if (document.getElementById("btnRevealHiddenText")) return;

        const btn = document.createElement("input");
        btn.id = "btnRevealHiddenText";
        btn.type = "button";
        btn.value = "Reveal Hidden";
        btn.style.color = "red";
        btn.style.float = "right";
        btn.style.marginLeft = "10px";

        btn.addEventListener("click", () => {
            if (!revealed) {
                revealHiddenTextInUSC();
                btn.value = "Hide Revealed";
            } else {
                undoReveal();
                btn.value = "Reveal Hidden";
            }
        });

        const download = document.getElementById("Download");

        if (download) {
            const dds = download.getElementsByTagName("dd");
            if (dds.length > 0) {
                dds[0].insertBefore(btn, dds[0].firstChild);
                return;
            }
        }

        Object.assign(btn.style, {
            position: "fixed",
            top: "80px",
            right: "20px",
            zIndex: "999999999",
            background: "white",
            border: "2px solid purple",
            padding: "4px 8px",
            cursor: "pointer"
        });

        document.body.appendChild(btn);
    }

    function startWatching() {
        addButtonOnlyIfHiddenFound();

        const observer = new MutationObserver(() => {
            if (!revealed) {
                addButtonOnlyIfHiddenFound();
            }
        });

        observer.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: [
                "style",
                "class",
                "hidden",
                "aria-hidden",
                "title",
                "alt",
                "aria-label"
            ]
        });
    }

    startWatching();

})();